iT邦幫忙

2025 iThome 鐵人賽

DAY 6
0
自我挑戰組

攜手 AI 從零開始打造一款 Flutter 應用程式系列 第 6 篇

Day 6: 讓畫面動起來 - ListView 與 Card 的列表魔法

  • 分享至 

  • xImage
  •  

前言

大家好,歡迎來到第六天的旅程!在 Day 5,我們成功地從一張白紙打造出「省錢拍拍」的首頁,掌握了 Column、Row 與 Container 的佈局技巧。

今天將學習 Flutter 中處理可滾動列表的王牌 Widget:ListView,並搭配 Card 與 ListTile,打造出一個既美觀又高效的動態交易紀錄列表。

Step 1: 為什麼 Column 不適用於長列表?

在 Flutter 中,Column 和 Row 會試圖一次性地計算並渲染其所有的子元件。當子元件的總高度(對 Column 而言)或總寬度(對 Row 而言)超過螢幕的顯示範圍時,就會發生 Overflow 錯誤。

雖然我們可以使用 SingleChildScrollView 來包裹 Column 讓它變得可以滾動,但這是一個效能不佳的做法。因為它依然會一次性渲染列表中的所有項目,即使是那些在螢幕外根本看不到的項目。如果列表有數百個項目,這將會造成嚴重的性能問題。

Step 2: 高效能列表的答案 - ListView.builder

ListView.builder 是一個「懶加載 (lazy-loading)」的列表建構器。它的核心理念是:只創建和渲染那些當前正顯示在螢幕上的列表項。當使用者滾動列表時,它會智慧地回收滑出螢幕的項目,並創建即將進入螢幕的新項目。這種機制確保了即使有成千上萬筆資料,App 也能保持流暢的滾動性能。

ListView.builder 有兩個必填的核心屬性:

  • itemCount: 告訴 ListView 總共有多少個項目。
  • itemBuilder: 一個回呼函式 (Callback Function),它會接收 context 和項目索引 index,並根據這個索引回傳對應的列表項 Widget。

Step 3: 準備我們的假資料 (Mock Data)

在能夠連接後端資料庫之前,我們先在程式中創建一些假資料來模擬消費紀錄。

  1. 在 lib/main.dart 檔案的頂部(import 語句下方),新增一個 Transaction 類別來定義我們資料的結構。
  2. 在 HomePage 類別的內部,創建一個假資料列表。

請注意:由於 transactions 列表是在程式運行時才被建立的(它並非一個編譯時期的常數),我們必須移除 HomePage Widget 建構子前面的 const 關鍵字,Transaction 類別的建構子也是同理。

// lib/main.dart (在 import 下方)
class Transaction {
  final String title;
  final String category;
  final double amount;
  final DateTime date;

  // 我們也移除這個建構子的 const,因為 DateTime.now() 不是常數
  Transaction({
    required this.title,
    required this.category,
    required this.amount,
    required this.date,
  });
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      // 關閉右上角的 Debug 標籤
      debugShowCheckedModeBanner: false,
      title: 'SnapSaver',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      // 移除 'const' 關鍵字
      home: HomePage(),
    );
  }
}

class HomePage extends StatelessWidget {
  // 關鍵修正:移除 'const' 關鍵字
  HomePage({super.key});

  // 建立一個假資料列表
  final List<Transaction> transactions = [
    Transaction(title: '7-Eleven 便利商店', category: '餐飲', amount: 85, date: DateTime.now()),
    Transaction(title: '全聯福利中心', category: '購物', amount: 750, date: DateTime.now()),
    Transaction(title: '週末電影票', category: '娛樂', amount: 540, date: DateTime.now()),
    Transaction(title: '捷運月票', category: '交通', amount: 1280, date: DateTime.now()),
    Transaction(title: '電信費', category: '生活繳費', amount: 499, date: DateTime.now()),
    Transaction(title: '午餐:牛肉麵', category: '餐飲', amount: 180, date: DateTime.now()),
    Transaction(title: '蝦皮購物', category: '購物', amount: 1250, date: DateTime.now()),
  ];

  @override
  Widget build(BuildContext context) {
    // ...
  }
}

Step 4: 實作動態列表

現在,萬事俱備。我們將 Column 佈局與 ListView 結合起來。

重要觀念:當你在一個 Column 內部直接放置 ListView 時,會發生錯誤。因為 Column 給予子元件無限的垂直空間,但 ListView 需要一個有限的、確切的高度。為了解決這個問題,我們需要用 Expanded Widget 來包裹 ListView,告訴它:「請填滿 Column 中所有剩餘的空間」。

  • Expanded 是一個 **彈性佈局 (Flexible Layout)**的 Widget,用來在 Row 或 Column 中,
    讓子元件自動填滿剩餘的可用空間。

  • Card 是 Flutter 提供的一種 UI 容器,
    它模仿了 Material Design 的卡片風格,有圓角、陰影,在視覺上提供層次感與區隔。

修改 HomePage 的 build 方法:

// 分隔線
  const Padding(
     padding: EdgeInsets.symmetric(horizontal: 16.0),
     child: Divider(),
  ),
  // 使用 Expanded 包裹 ListView.builder
  Expanded(
    child: ListView.builder(
      itemCount: transactions.length,
      itemBuilder: (BuildContext context, int index) {
        final transaction = transactions[index];
        return Card(
          margin: const EdgeInsets.symmetric(
            horizontal: 16.0,
            vertical: 6.0,
          ),
          child: ListTile(
            leading: const Icon(
              Icons.receipt_long,
              color: Colors.deepPurple,
            ),
            title: Text(transaction.title),
            subtitle: Text(transaction.category),
            trailing: Text(
              'NT\$ ${transaction.amount.toStringAsFixed(0)}',
              style: const TextStyle(
                color: Colors.redAccent,
                fontWeight: FontWeight.bold,
              ),
            ),
          ),
        );
      },
    ),
  ),

ListView

程式碼解析:

  1. 在 Column 的最下方,我們使用 Expanded 來包裹 ListView.builder,解決了高度限制的問題。
  2. itemCount 直接設為我們假資料列表的長度。
  3. 在 itemBuilder 中,我們為每個列表項返回一個 Card Widget,內部包裹 ListTile 來呈現資料。
  4. Card 內部包裹著 ListTile,它極大地簡化了列表項的佈局。我們將 title, category, amount 等資料輕鬆地放入 ListTile 的 title, subtitle, trailing 屬性中。

今日結語

今天我們學習了 Flutter 中,處理長列表最高效的元件之一:ListView.builder。不僅理解了它「懶加載」的核心原理,還學會了使用 Expanded 來處理它在 Column 中的佈局問題,並搭配 Card 和 ListTile 打造出專業、美觀的列表項。

明天,我們將來處理 App 的「門面」問題,深入探索 ThemeData,為「省錢拍拍」客製化一套專屬的色彩與字體主題。


上一篇
Day 5: UI 佈局基礎 - Row, Column, Container
下一篇
Day 7: App 的整體造型師 - 使用 ThemeData 打造專屬風格
系列文
攜手 AI 從零開始打造一款 Flutter 應用程式 共 30 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言